﻿using FunctionScope.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace FunctionScope.Views
{
    public class Trace2DView : FrameworkElement
    {
        static Trace2DView()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Trace2DView), new FrameworkPropertyMetadata(typeof(Trace2DView)));
        }

        public static readonly int NUMPERSISTPENS = 16;

        private VisualCollection _childVisuals;
        private DrawingVisual _trace;
        private DrawingVisual _graticule;

        private Pen _graticulePen;
        private SolidColorBrush _graticuleBrush;
        private SolidColorBrush _backBrush;

        private DispatcherTimer _paintTimer;

        private ColouredGeometry[] _traceSegments;

        private class ColouredGeometry {
            public Pen Pen {get;set;}
            public StreamGeometry Geom { get; set; }
            public StreamGeometryContext GeomContext { get; set; }
        }

        public Trace2DView()
        {
            _childVisuals = new VisualCollection(this);
            _trace = new DrawingVisual();
            _graticule = new DrawingVisual();

            _childVisuals.Add(_trace);
            _childVisuals.Add(_graticule);

            _backBrush = new SolidColorBrush(Color.FromArgb(255, 0, 32, 0));
            _backBrush.Freeze();

            _graticuleBrush = new SolidColorBrush(Color.FromArgb(255, 0, 64, 0));
            _graticuleBrush.Freeze();
            _graticulePen = new Pen(_graticuleBrush, 1);
            _graticulePen.Freeze();

            //50Hz paint timer
            _paintTimer = new DispatcherTimer(new TimeSpan(10000 * 20), DispatcherPriority.Render, PaintTrace, Dispatcher);

            //Create trace segment cache for persistance. This prevents object creation where possible during paint
            _traceSegments = new ColouredGeometry[NUMPERSISTPENS];
            for (int i = 0; i< _traceSegments.Length; i++)
            {
                byte alpha = (byte)(((double)i / (double)_traceSegments.Length) * 255.0);
                Brush b = new SolidColorBrush(Color.FromArgb(alpha, 0, 255, 0));
                b.Freeze();

                Pen p = new Pen(b, 2);

                //Set line caps to prevent mitres from extending past the bounds of the plotted function
                p.EndLineCap = PenLineCap.Flat;
                p.StartLineCap = PenLineCap.Flat;
                p.LineJoin = PenLineJoin.Bevel;

                p.Freeze();

                _traceSegments[i] = new ColouredGeometry() { Pen = p, Geom = new StreamGeometry()};

            }


            
        }

        private void EnableSubPixelRendering(bool enable)
        {
            if (enable)
            {
                SnapsToDevicePixels = false;
                VisualEdgeMode = EdgeMode.Unspecified;
            }
            else
            {
                SnapsToDevicePixels = true;
                VisualEdgeMode = EdgeMode.Aliased;
            }
        }

        public void PaintTrace(object state, EventArgs e)
        {
            if (DataContext is Trace2DPlotSettingsModel)
            {
                //Get trace data
                Trace2DPlotSettingsModel model = DataContext as Trace2DPlotSettingsModel;
                TraceModel<double> data = model.Trace;

                if (model == null || model.Trace == null)
                    return;

                //Set rendering quality
                EnableSubPixelRendering(model.SubpixelRendering);

                try {
                    using (DrawingContext dc = _trace.RenderOpen())
                    {
                        lock (data.SyncRoot)
                        {
                            Point p1 = new Point();
                            Point p2 = new Point();
                            int c = 0;

                            //Background
                            dc.DrawRectangle(_backBrush, null, new Rect(this.RenderSize));

                            //Open geometry for edit
                            foreach (ColouredGeometry seg in _traceSegments)
                            {
                                seg.GeomContext = seg.Geom.Open();
                            }

                            //Should be integer. TODO: marry up length of trace segments here with the settings in the sampler
                            int samplePerSegment = data.Length / _traceSegments.Length;

                            foreach (TraceSampleModel<double> sample in data)
                            {
                                //Ignore invalid points
                                if (sample.T == 0.0)
                                    continue;

                                ProjectPoint(data.MinX, data.MinY, data.MaxX, data.MaxY, sample.X, sample.Y, ref p1);

                                //Find segment this belongs to
                                int seg = c / samplePerSegment;
                                ColouredGeometry cg = _traceSegments[seg];

                                if (c % samplePerSegment == 0)
                                {
                                    //Start of a new segment, clear any residual geometry. TODO. If the trace data is also paged we can save some
                                    //time by not updating pages that havn't changed.
                                    cg.Geom.Clear();

                                    if (c == 0)
                                    {
                                        //First point in entire trace
                                        cg.GeomContext.BeginFigure(p1, false, false);
                                    }
                                    else
                                    {
                                        //First point in next segment, ensure segments join correctly by starting this 
                                        //segment at the last location of the previous segment (p2)
                                        cg.GeomContext.BeginFigure(p2, false, false);
                                        cg.GeomContext.LineTo(p1, true, false);
                                    }

                                }
                                else
                                {
                                    //Continuation in a segment
                                    cg.GeomContext.LineTo(p1, true, false);
                                    
                                }

                                p2 = p1;
                                c++;
                            }

                            //CLose each segment and paint it
                            foreach (ColouredGeometry seg in _traceSegments)
                            {
                                seg.GeomContext.Close();
                                dc.DrawGeometry(null, seg.Pen, seg.Geom);
                            }

                        }
                    }

                }
                catch (Exception ex)
                {
                    Console.Out.WriteLine("Failed to paint trace " + ex.ToString());
                }

                //Check if graticule should be painted
                if (model.ShowGraticule == true)
                {
                    if (!_childVisuals.Contains(_graticule))
                        _childVisuals.Add(_graticule);
                } else
                {
                    if (_childVisuals.Contains(_graticule))
                        _childVisuals.Remove(_graticule);
                }

            }

        }
        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            //Update graticule
            PaintGraticule();
        }

        public void PaintGraticule()
        {
            using (DrawingContext dc = _graticule.RenderOpen())
            {

                //Set scale. This also scales the line widths, which we dont want
                //dc.PushTransform(new ScaleTransform(ActualWidth, ActualHeight));

                //Vertical
                for (int i = 0; i<=10; i++)
                {
                    Point p1 = new Point(0, (double)i / 10d);
                    Point p2 = new Point(1, (double)i / 10d);

                    p1.X *= ActualWidth;
                    p1.Y *= ActualHeight;

                    p2.X *= ActualWidth;
                    p2.Y *= ActualHeight;

                    dc.DrawLine(_graticulePen, p1, p2);
                }

                //Horizontal
                for (int i = 0; i <= 10; i++)
                {
                    Point p1 = new Point((double)i / 10d, 0);
                    Point p2 = new Point((double)i / 10d, 1);

                    p1.X *= ActualWidth;
                    p1.Y *= ActualHeight;

                    p2.X *= ActualWidth;
                    p2.Y *= ActualHeight;

                    dc.DrawLine(_graticulePen, p1, p2);
                }



            }
        }

        /// <summary>
        /// Todo, delegate to an Axis object once axis are available
        /// </summary>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="maxX"></param>
        /// <param name="maxY"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="p"></param>
        private void ProjectPoint(double minX, double minY, double maxX, double maxY, double x, double y, ref Point p)
        {
            p.X = ((x - minX) / (maxX - minX)) * this.ActualWidth;
            p.Y = this.ActualHeight - (((y - minY) / (maxY - minY)) * this.ActualHeight); //Y 0 is top

            if (SnapsToDevicePixels)
            {
                //Snap to whole pixels
                p.X = Math.Round(p.X, MidpointRounding.AwayFromZero);
                p.Y = Math.Round(p.Y, MidpointRounding.AwayFromZero);
            }
        }

        


        protected override Visual GetVisualChild(int index)
        {
            if (index < 0 || index >= _childVisuals.Count)
                throw new ArgumentOutOfRangeException();

            return _childVisuals[index];
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return _childVisuals.Count;
            }
        }
    }
}
